import { forEach as _forEach } from 'lodash-es';
import { MultipartUploadVerifyOutput } from '@shared/service-proxies/service-proxies';
import { FileUploadHelper } from '@shared/helpers/FileUploadHelper';
import { filter as _filter } from 'lodash-es';

import { AppNotifyService } from '@shared/utils/service/app-notify.service';
import { FileItem, FileUploader, FileUploaderOptions, ParsedResponseHeaders } from 'ng2-file-upload';
//import * as SparkMD5 from 'spark-md5';
import * as jsSHA from "jssha"
import { ChunkContainer, FileChunk, MultipartUploadOption, Status } from './multipart-upload-option';
import { IAjaxResponse, TokenService } from 'abp-ng2-module';

export class MultipartUploadHelper {

    maxParallelCount = 4;

    uploaderList: FileUploader[] = [];
    singleUploader: FileUploader

    // 是否可以上传（判断是否正在上传中）
    showFileUpload: boolean = false;
    
    // 当前正在上传的文件
    container: ChunkContainer = null;
    
    // 等待上传的文件
    uploadingFileQueue: ChunkContainer[] = [];

    // 文件上传分块
    chunks: FileChunk[] = [];
   
    // 分块上传的重试次数记录 
    private retryArr: { [key:number]: number} = {};
    
    showHashProgress: boolean = false;
    showMerging: boolean = false;
    uploadProgress: number = 0;

    // 默认状态
    status: Status = Status.wait;

    private uploaderOptions: FileUploaderOptions = {};

    private Option: MultipartUploadOption;

    constructor(
        private notify: AppNotifyService,
        private _tokenService: TokenService) {

    }

    public Initialize(maxParallelCount: number, option: MultipartUploadOption) {
        
        this.Option = option;

        this.status = Status.wait;
        this.uploadProgress = 0;
        this.showMerging = false;
        this.showHashProgress = false;
        this.retryArr = {};
        this.chunks = [];

        this.uploadingFileQueue = [];
        this.container = null;
        this.showFileUpload = false;
        this.singleUploader = null;
        this.uploaderList = [];

        this.uploaderOptions.autoUpload = false;
        this.uploaderOptions.authToken = 'Bearer ' + this._tokenService.getToken();
        this.uploaderOptions.removeAfterUpload = true;

        for (let i = 0; i < maxParallelCount; i++) {
            let uploader = this.createPartUploader();
            this.uploaderList.push(uploader);
        }
        this.singleUploader = this.createSingleUploader();
    }

    // 移除一个待上传的文件
    public cancelFromQueue(index) {
        this.uploadingFileQueue.splice(index, 1);
    }

    public handleUpload() {
        if (this.status === Status.uploading || this.showMerging) {
            this.notify.info("正在上传中");
            return;
        }

        if (this.container === null && this.uploadingFileQueue.length > 0) {
            this.container = this.uploadingFileQueue.shift();
        }

        if (this.container === null || !this.container.file) {
            this.notify.info("请选择文件");
            return;
        }

        //return;

        this.status = Status.uploading;
        if (!this.showFileUpload) {
            this.showFileUpload = true;
        }

        this.showHashProgress = true;

        this.calculateHashSample(this.container.file)
            .then((fileSha1) => {
                this.showHashProgress = false;
                this.container.hash = fileSha1;

                //console.log("hashSample", this.container.hash);

                if (this.container.isMultiPartUpload) {
                    this.verify(this.container, (data: any) => {
                        const { uploaded, uploadedList, fileItem } = data;

                        if (uploaded) {
                            this.notify.success("秒传:上传成功");
                            this.processUploadResult(fileItem);
                            return;
                        }

                        this.chunks = this.createFileChunk(this.container.file);

                        this.chunks.forEach(t => {
                            const chunkName = this.container.hash + "-" + t.index;
                            t.fileHash = this.container.hash;
                            t.hash = chunkName;
                            if (this.checkChunkState(uploadedList, t)) {
                                t.progress = 100;
                                t.status = Status.done;
                            } else {
                                t.progress = 0;
                                t.status = Status.wait;
                            }
                        })

                        this.uploadChunks();
                    })
                } else {
                    let fileItem = new FileItem(this.singleUploader, this.container.file, this.uploaderOptions);
                    fileItem.alias = this.container.hash;
                    this.singleUploader.queue.push(fileItem);
                    this.singleUploader.uploadItem(fileItem);
                }
            });
    }
    public handleResume() {
        this.status = Status.uploading;

        if (this.container && this.container.hash && this.container.isMultiPartUpload) {
            this.verify(this.container, (data: any) => {
                this.chunks.forEach(t => {
                    if (this.checkChunkState(data.uploadedList, t)) {
                        t.progress = 100;
                        t.status = Status.done;
                    } else {
                        t.progress = 0;
                        t.status = Status.wait;
                    }
                })
                this.uploadChunks();
            });
        } else {
            this.handleUpload();
        }
    }

    public handlePause() {
        this.status = Status.pause;
        this.uploaderList.forEach(t => {
            t.queue.forEach(fileUp => fileUp.cancel());
        });
    }

    public getUploadingFileItem(file: File) {
        let exName = FileUploadHelper.getExName(file.name);
        let item:any = {
            file: file,
            hash: '',
            dimensions: '',
            exName: exName,
            dataUrl: '/assets/common/images/purpleLoading.gif',
            icon: FileUploadHelper.getIcon(exName),
            isImage: FileUploadHelper.isImage(exName),
            fileSizeStr: FileUploadHelper.getFileSize(file.size),
            isMultiPartUpload: (file.size > 1024 * 1024 * 20)
        };
        if (item.isImage) {
            // svg可以取到宽高，但不能预览

            FileUploadHelper.getBase64(item.file).subscribe(tt => {
                item.dataUrl = tt;

                let image = new Image();
                image.src = tt;
                image.onload = function () {
                    item.dimensions = image.width + "x" + image.height;
                };
            });
        } else if (FileUploadHelper.isSvg(exName)) {
            item.icon = "bi bi-file-earmark";
        }

        item.customData = {};
        return item;
    }


    // 计算sha1
    /**
     * name
     */
    public calculateHashSample(file) {
        return new Promise<string>(resolve => {
            //const spark = new SparkMD5.ArrayBuffer();
            const reader = new FileReader();
            //const file = this.container.file;
            // 文件大小
            const size = file.size;
            let offset = 2 * 1024 * 1024;

            let chunks = [file.slice(0, offset)];

            // 前面100K

            let cur = offset;
            while (cur < size) {
                // 最后一块全部加进来
                if (cur + offset >= size) {
                    chunks.push(file.slice(cur, cur + offset));
                } else {
                    // 中间的 前中后去两个子杰
                    const mid = cur + offset / 2;
                    const end = cur + offset;
                    chunks.push(file.slice(cur, cur + 2));
                    chunks.push(file.slice(mid, mid + 2));
                    chunks.push(file.slice(end - 2, end));
                }
                // 前取两个子杰
                cur += offset;
            }
            // 拼接
            reader.readAsArrayBuffer(new Blob(chunks));

            // 最后100K
            reader.onload = e => {
                var sha1 = new jsSHA.default('SHA-1', 'ARRAYBUFFER');
                sha1.update(e.target.result as ArrayBuffer);
                var preid = sha1.getHash('HEX', {
                    outputUpper: true
                });
                resolve(preid);
                //spark.append(e.target.result as ArrayBuffer);
                //resolve(spark.end());
            };
        });
    }

    // 分片上传
    private uploadChunks() {
        this.retryArr = [];
        try {
            let index = this.chunks.findIndex(v => v.status == Status.wait || v.status == Status.error);
            if (index < 0) {
                this.mergeRequest();
            }

            this.uploaderList.forEach(t => {
                this.addNewTask(t);
            });

        } catch (e) {
            // 上传有被reject的
            this.notify.error("上传失败了,考虑重试下吧");
        }
    }

    // 分片上传的 Uploader
    private createPartUploader() {
        let uploader = new FileUploader({
            url: this.Option.partUploaderUrl,
            method: 'POST',
        });

        uploader.onAfterAddingFile = (file) => {
            file.withCredentials = false;
        };

        uploader.onBuildItemForm = (fileItem: FileItem, form: any) => {
            var item = this.chunks.find(t => t.hash === fileItem.alias);
            if (item) {
                form.append("index", item.index);
                form.append("count", this.chunks.length);
                form.append("fileHash", this.container.hash);
            }
        };

        uploader.onSuccessItem = (fileItem, response, status, headers) => {
            const resp = <IAjaxResponse>JSON.parse(response);
            if (resp.success) {
                this.onUploadSuccess(fileItem, uploader);
            } else {
                this.onUploadError(fileItem, resp, uploader);
            }
        };
        uploader.onProgressItem = (item, progress) => {
            this.updateProgress(item, progress);
        };
        uploader.onErrorItem = (fileItem, response, status, headers) => {
            const resp = <IAjaxResponse>JSON.parse(response);
            this.onUploadError(fileItem, resp, uploader);
        };

        uploader.onCompleteAll = () => {
            this.onCompleteAll();
        };
        uploader.onCancelItem = (fileItem: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
            var item = this.chunks.find(t => t.hash === fileItem.alias);
            if (item) {
                item.status = Status.wait;
                item.progress = 0;
            }
        };

        uploader.setOptions(this.uploaderOptions);
        return uploader;
    }

    private updateProgress(fileItem: FileItem, val: number) {
        var item = this.chunks.find(t => t.hash === fileItem.alias);
        if (item) {
            item.progress = val;

            if (!this.container.file || !this.chunks.length) {
                return 0;
            }
            const loaded = this.chunks
                .map(item => item.size * item.progress)
                .reduce((acc, cur) => acc + cur);

            let str = ((loaded / this.container.file.size) - 0.01).toFixed(2);
            this.uploadProgress = parseFloat(str);
        }
    }
    private onUploadSuccess(fileItem: FileItem, uploader: FileUploader) {
        var item = this.chunks.find(t => t.hash === fileItem.alias);
        if (item) {
            item.status = Status.done;
            item.progress = 100;

            this.addNewTask(uploader);
        }
    }

    private addNewTask(uploader: FileUploader) {
        if (this.status === Status.pause) {
            return;
        }
        let index = this.chunks.findIndex(v => v.status == Status.wait || v.status == Status.error);
        if (index >= 0) {
            //console.log(index, "start");

            let currentItem = this.chunks[index];
            currentItem.status = Status.uploading

            let file = this.container.file.slice(currentItem.startCur, currentItem.endCur);

            let fileItem = new FileItem(uploader, file, this.uploaderOptions);
            fileItem.alias = currentItem.hash;
            uploader.queue.push(fileItem);
            uploader.uploadItem(fileItem);
        }
    }

    private onUploadError(fileItem: FileItem, resp: IAjaxResponse, uploader: FileUploader) {
        //this.notify.error(resp.error.message);
        var item = this.chunks.find(t => t.hash === fileItem.alias);
        if (item) {
            item.status = Status.error;
            item.progress = 0;

            if (typeof this.retryArr[item.index] !== 'number') {
                this.retryArr[item.index] = 0;
            }
            this.retryArr[item.index]++;
            if (this.retryArr[item.index] >= 5) {
                item.status = Status.reject;
            }

            this.addNewTask(uploader);
        }
    }
    private onCompleteAll() {
        let iii = this.chunks.findIndex(v => v.status != Status.done);
        if (iii < 0) {
            this.mergeRequest();
        }
    }

    // 分片上传的 Uploader end

    // 小文件上传
    private createSingleUploader() {
        let uploader = new FileUploader({
            url: this.Option.uploaderUrl,
            method: 'POST',
        });

        uploader.onAfterAddingFile = (file) => {
            file.withCredentials = false;
        };

        uploader.onBuildItemForm = (fileItem: FileItem, form: any) => {
            this.Option.singleUploaderOnBuildItemForm(fileItem, form);
        };

        uploader.onSuccessItem = (fileItem, response, status, headers) => {
            const resp = <IAjaxResponse>JSON.parse(response);
            if (resp.success) {
                this.processUploadResult(resp.result);
            } else {
                this.notify.error(resp.error.message);
                this.status = Status.error;
            }
        };
        uploader.onProgressItem = (item, progress) => {
            this.uploadProgress = progress;
        };

        uploader.onErrorItem = (fileItem, response, status, headers) => {
            const resp = <IAjaxResponse>JSON.parse(response);
            this.notify.error(resp.error.message);
            this.status = Status.error;
        };

        uploader.setOptions(this.uploaderOptions);
        return uploader;
    }

    private processUploadResult(fileItem: any) {
        let pause = this.status === Status.pause;
        this.status = Status.done;
        this.showMerging = false;

        this.Option.uploadedCallback(fileItem);
        this.container = null;
        this.uploadProgress = 0;
 
        if (this.uploadingFileQueue.length > 0) {
            if (!pause) {
                this.handleUpload();
            }
        }
    }

    // tiqu
    private mergeRequest() {
        if (this.status === Status.done) {
            return;
        }
        this.showMerging = true;

        var param = this.Option.getMergeRequestInput();

        this.Option.mergeRequestFunction(param)
            .subscribe((result) => {
                this.processUploadResult(result.fileItem);
            }, (error) => {
                this.showMerging = false;
                this.status = Status.error;
            });
    }

    // 分片上传前，检查上传情况（断点续传or秒传）
    private verify(fileContainer: ChunkContainer, callback: { (result: MultipartUploadVerifyOutput): void }) {
        var param = this.Option.getVerifyInput(fileContainer);

        this.Option.verifyFunction(param)
            .subscribe((result) => {
                callback(result);
            }, error => {
                this.status = Status.error;
            });
    }

    // 文件切片
    private createFileChunk(file: File, size = 0) {
        const SIZE = 5 * 1024 * 1024;
        if (size <= 0) {
            size = SIZE;
        }

        // 生成文件块
        const chunks: FileChunk[] = [];
        let cur = 0;
        while (cur < file.size) {
            let end = cur + size;
            if (end > file.size) {
                end = file.size;
            }
            let chunkSize = end - cur;

            chunks.push({
                fileHash: '',
                startCur: cur,
                endCur: end,
                index: chunks.length,
                hash: '',
                progress: 0,
                size: chunkSize,
                status: Status.wait
            });

            cur += size;
        }
        return chunks;
    }

    private checkChunkState(uploadedList: { [key: number]: number }, chunk: FileChunk): boolean {
        var count = uploadedList[chunk.index];
        if (count && count === chunk.size) {
            return true;
        }

        return false;
    }
}
